{
 "cells": [
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<font color=\"red\">注</font>: 使用 tensorboard 可视化需要安装 tensorflow (TensorBoard依赖于tensorflow库，可以任意安装tensorflow的gpu/cpu版本)\n",
    "\n",
    "```shell\n",
    "pip install tensorflow-cpu\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-02T12:57:00.481985Z",
     "start_time": "2025-02-02T12:56:56.420920Z"
    }
   },
   "source": [
    "import matplotlib as mpl\n",
    "import matplotlib.pyplot as plt\n",
    "%matplotlib inline\n",
    "import numpy as np\n",
    "import sklearn\n",
    "import pandas as pd\n",
    "import os\n",
    "import sys\n",
    "import time\n",
    "from tqdm.auto import tqdm\n",
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.nn.functional as F\n",
    "\n",
    "print(sys.version_info)\n",
    "for module in mpl, np, pd, sklearn, torch:\n",
    "    print(module.__name__, module.__version__)\n",
    "    \n",
    "device = torch.device(\"cuda:0\") if torch.cuda.is_available() else torch.device(\"cpu\")\n",
    "print(device)\n",
    "\n",
    "seed = 42\n"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sys.version_info(major=3, minor=12, micro=3, releaselevel='final', serial=0)\n",
      "matplotlib 3.10.0\n",
      "numpy 1.26.4\n",
      "pandas 2.2.3\n",
      "sklearn 1.6.1\n",
      "torch 2.5.1+cpu\n",
      "cpu\n"
     ]
    }
   ],
   "execution_count": 1
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 数据准备\n",
    "\n",
    "```shell\n",
    "$ tree -L 2 archive \n",
    "archive\n",
    "├── monkey_labels.txt\n",
    "├── training\n",
    "│   ├── n0\n",
    "│   ├── n1\n",
    "│   ├── n2\n",
    "│   ├── n3\n",
    "│   ├── n4\n",
    "│   ├── n5\n",
    "│   ├── n6\n",
    "│   ├── n7\n",
    "│   ├── n8\n",
    "│   └── n9\n",
    "└── validation\n",
    "    ├── n0\n",
    "    ├── n1\n",
    "    ├── n2\n",
    "    ├── n3\n",
    "    ├── n4\n",
    "    ├── n5\n",
    "    ├── n6\n",
    "    ├── n7\n",
    "    ├── n8\n",
    "    └── n9\n",
    "\n",
    "22 directories, 1 file\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-02T12:57:02.057198Z",
     "start_time": "2025-02-02T12:57:00.482972Z"
    }
   },
   "source": [
    "from torchvision import datasets\n",
    "from torchvision.transforms import ToTensor, Resize, Compose, ConvertImageDtype, Normalize\n",
    "\n",
    "\n",
    "from pathlib import Path\n",
    "\n",
    "DATA_DIR = Path(\"./archive/\")\n",
    "\n",
    "class MonkeyDataset(datasets.ImageFolder):\n",
    "    def __init__(self, mode, transform=None):\n",
    "        if mode == \"train\":\n",
    "            root = DATA_DIR / \"training\"\n",
    "        elif mode == \"val\":\n",
    "            root = DATA_DIR / \"validation\"\n",
    "        else:\n",
    "            raise ValueError(\"mode should be one of the following: train, val, but got {}\".format(mode))\n",
    "        super().__init__(root, transform)\n",
    "        self.imgs = self.samples\n",
    "        self.targets = [s[1] for s in self.samples]\n",
    "\n",
    "# resnet 要求的，见 https://pytorch.org/vision/stable/models/generated/torchvision.models.resnet50.html\n",
    "img_h, img_w = 224, 224\n",
    "transform = Compose([\n",
    "     Resize((img_h, img_w)),\n",
    "     ToTensor(),\n",
    "     Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),\n",
    "     ConvertImageDtype(torch.float),\n",
    "])\n",
    "\n",
    "\n",
    "train_ds = MonkeyDataset(\"train\", transform=transform)\n",
    "val_ds = MonkeyDataset(\"val\", transform=transform)\n",
    "\n",
    "print(\"load {} images from training dataset\".format(len(train_ds)))\n",
    "print(\"load {} images from validation dataset\".format(len(val_ds)))"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "load 1097 images from training dataset\n",
      "load 272 images from validation dataset\n"
     ]
    }
   ],
   "execution_count": 2
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-02T12:57:02.062241Z",
     "start_time": "2025-02-02T12:57:02.058194Z"
    }
   },
   "source": [
    "# 数据类别\n",
    "train_ds.classes, train_ds.class_to_idx"
   ],
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(['n0', 'n1', 'n2', 'n3', 'n4', 'n5', 'n6', 'n7', 'n8', 'n9'],\n",
       " {'n0': 0,\n",
       "  'n1': 1,\n",
       "  'n2': 2,\n",
       "  'n3': 3,\n",
       "  'n4': 4,\n",
       "  'n5': 5,\n",
       "  'n6': 6,\n",
       "  'n7': 7,\n",
       "  'n8': 8,\n",
       "  'n9': 9})"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 3
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-02T12:57:02.105666Z",
     "start_time": "2025-02-02T12:57:02.062739Z"
    }
   },
   "source": [
    "# 图片路径 及 标签\n",
    "for fpath, label in train_ds.imgs:\n",
    "    print(fpath, label)\n",
    "    break\n",
    "\n",
    "for img, label in train_ds:\n",
    "    # c, h, w  label\n",
    "    print(img.shape, label)\n",
    "    break"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "archive\\training\\n0\\n0018.jpg 0\n",
      "torch.Size([3, 224, 224]) 0\n"
     ]
    }
   ],
   "execution_count": 4
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-02T12:57:02.110426Z",
     "start_time": "2025-02-02T12:57:02.105666Z"
    }
   },
   "source": [
    "# 遍历train_ds得到每张图片，计算每个通道的均值和方差\n",
    "def cal_mean_std(ds):\n",
    "    mean = 0.\n",
    "    std = 0.\n",
    "    for img, _ in ds:\n",
    "        mean += img.mean(dim=(1, 2))\n",
    "        std += img.std(dim=(1, 2))\n",
    "    mean /= len(ds)\n",
    "    std /= len(ds)\n",
    "    return mean, std\n",
    "\n",
    "# 经过 normalize 后 均值为0，方差为1\n",
    "# print(cal_mean_std(train_ds))"
   ],
   "outputs": [],
   "execution_count": 5
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-02T12:57:02.120170Z",
     "start_time": "2025-02-02T12:57:02.112196Z"
    }
   },
   "source": [
    "import torch.nn as nn\n",
    "from torch.utils.data.dataloader import DataLoader    \n",
    "\n",
    "batch_size = 16\n",
    "# 从数据集到dataloader\n",
    "train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True)\n",
    "val_loader = DataLoader(val_ds, batch_size=batch_size, shuffle=False)"
   ],
   "outputs": [],
   "execution_count": 6
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-02T12:57:02.289582Z",
     "start_time": "2025-02-02T12:57:02.121166Z"
    }
   },
   "source": [
    "for imgs, labels in train_loader:\n",
    "    print(imgs.shape)\n",
    "    print(labels.shape)\n",
    "    break"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "torch.Size([16, 3, 224, 224])\n",
      "torch.Size([16])\n"
     ]
    }
   ],
   "execution_count": 7
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 定义模型"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-02T12:57:02.651152Z",
     "start_time": "2025-02-02T12:57:02.289582Z"
    }
   },
   "source": [
    "from torchvision.models import resnet50\n",
    "\n",
    "\n",
    "# 加载resnet50模型以及预训练权重，冻结除最后一层外所有层的权重，去除最后一层并添加自定义分类层\n",
    "class ResNet50(nn.Module):\n",
    "    def __init__(self, num_classes=10, frozen=True):\n",
    "        super().__init__()\n",
    "        # 下载预训练权重\n",
    "        self.model = resnet50(weights=\"IMAGENET1K_V2\",) # 这里的weights参数可以选择\"IMAGENET1K_V2\"或None，None表示随机初始化\n",
    "        # 冻结前面的层\n",
    "        if frozen:\n",
    "            for param in self.model.parameters():\n",
    "                param.requires_grad = False # 冻结权重\n",
    "        for name, param in self.model.named_parameters():\n",
    "            if name == \"layer4.2.conv3.weight\":\n",
    "                param.requires_grad = True # 解冻该层\n",
    "        # 添加自定义分类层\n",
    "        # print(self.model)\n",
    "        print(self.model.fc.in_features) # 打印resnet50的最后一层的输入通道数\n",
    "        print(self.model.fc.out_features) # 打印resnet50的最后一层的输出通道数 1000\n",
    "        self.model.fc = nn.Linear(self.model.fc.in_features, num_classes)   # 自定义分类层,把resnet50的最后一层改为num_classes个输出\n",
    "        \n",
    "        \n",
    "    def forward(self, x):\n",
    "        return self.model(x)\n",
    "\n",
    "\n",
    "for idx, (key, value) in enumerate(ResNet50().named_parameters()):\n",
    "    print(f\"{key:^40}paramerters num: {np.prod(value.shape)}\")\n"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2048\n",
      "1000\n",
      "           model.conv1.weight           paramerters num: 9408\n",
      "            model.bn1.weight            paramerters num: 64\n",
      "             model.bn1.bias             paramerters num: 64\n",
      "      model.layer1.0.conv1.weight       paramerters num: 4096\n",
      "       model.layer1.0.bn1.weight        paramerters num: 64\n",
      "        model.layer1.0.bn1.bias         paramerters num: 64\n",
      "      model.layer1.0.conv2.weight       paramerters num: 36864\n",
      "       model.layer1.0.bn2.weight        paramerters num: 64\n",
      "        model.layer1.0.bn2.bias         paramerters num: 64\n",
      "      model.layer1.0.conv3.weight       paramerters num: 16384\n",
      "       model.layer1.0.bn3.weight        paramerters num: 256\n",
      "        model.layer1.0.bn3.bias         paramerters num: 256\n",
      "   model.layer1.0.downsample.0.weight   paramerters num: 16384\n",
      "   model.layer1.0.downsample.1.weight   paramerters num: 256\n",
      "    model.layer1.0.downsample.1.bias    paramerters num: 256\n",
      "      model.layer1.1.conv1.weight       paramerters num: 16384\n",
      "       model.layer1.1.bn1.weight        paramerters num: 64\n",
      "        model.layer1.1.bn1.bias         paramerters num: 64\n",
      "      model.layer1.1.conv2.weight       paramerters num: 36864\n",
      "       model.layer1.1.bn2.weight        paramerters num: 64\n",
      "        model.layer1.1.bn2.bias         paramerters num: 64\n",
      "      model.layer1.1.conv3.weight       paramerters num: 16384\n",
      "       model.layer1.1.bn3.weight        paramerters num: 256\n",
      "        model.layer1.1.bn3.bias         paramerters num: 256\n",
      "      model.layer1.2.conv1.weight       paramerters num: 16384\n",
      "       model.layer1.2.bn1.weight        paramerters num: 64\n",
      "        model.layer1.2.bn1.bias         paramerters num: 64\n",
      "      model.layer1.2.conv2.weight       paramerters num: 36864\n",
      "       model.layer1.2.bn2.weight        paramerters num: 64\n",
      "        model.layer1.2.bn2.bias         paramerters num: 64\n",
      "      model.layer1.2.conv3.weight       paramerters num: 16384\n",
      "       model.layer1.2.bn3.weight        paramerters num: 256\n",
      "        model.layer1.2.bn3.bias         paramerters num: 256\n",
      "      model.layer2.0.conv1.weight       paramerters num: 32768\n",
      "       model.layer2.0.bn1.weight        paramerters num: 128\n",
      "        model.layer2.0.bn1.bias         paramerters num: 128\n",
      "      model.layer2.0.conv2.weight       paramerters num: 147456\n",
      "       model.layer2.0.bn2.weight        paramerters num: 128\n",
      "        model.layer2.0.bn2.bias         paramerters num: 128\n",
      "      model.layer2.0.conv3.weight       paramerters num: 65536\n",
      "       model.layer2.0.bn3.weight        paramerters num: 512\n",
      "        model.layer2.0.bn3.bias         paramerters num: 512\n",
      "   model.layer2.0.downsample.0.weight   paramerters num: 131072\n",
      "   model.layer2.0.downsample.1.weight   paramerters num: 512\n",
      "    model.layer2.0.downsample.1.bias    paramerters num: 512\n",
      "      model.layer2.1.conv1.weight       paramerters num: 65536\n",
      "       model.layer2.1.bn1.weight        paramerters num: 128\n",
      "        model.layer2.1.bn1.bias         paramerters num: 128\n",
      "      model.layer2.1.conv2.weight       paramerters num: 147456\n",
      "       model.layer2.1.bn2.weight        paramerters num: 128\n",
      "        model.layer2.1.bn2.bias         paramerters num: 128\n",
      "      model.layer2.1.conv3.weight       paramerters num: 65536\n",
      "       model.layer2.1.bn3.weight        paramerters num: 512\n",
      "        model.layer2.1.bn3.bias         paramerters num: 512\n",
      "      model.layer2.2.conv1.weight       paramerters num: 65536\n",
      "       model.layer2.2.bn1.weight        paramerters num: 128\n",
      "        model.layer2.2.bn1.bias         paramerters num: 128\n",
      "      model.layer2.2.conv2.weight       paramerters num: 147456\n",
      "       model.layer2.2.bn2.weight        paramerters num: 128\n",
      "        model.layer2.2.bn2.bias         paramerters num: 128\n",
      "      model.layer2.2.conv3.weight       paramerters num: 65536\n",
      "       model.layer2.2.bn3.weight        paramerters num: 512\n",
      "        model.layer2.2.bn3.bias         paramerters num: 512\n",
      "      model.layer2.3.conv1.weight       paramerters num: 65536\n",
      "       model.layer2.3.bn1.weight        paramerters num: 128\n",
      "        model.layer2.3.bn1.bias         paramerters num: 128\n",
      "      model.layer2.3.conv2.weight       paramerters num: 147456\n",
      "       model.layer2.3.bn2.weight        paramerters num: 128\n",
      "        model.layer2.3.bn2.bias         paramerters num: 128\n",
      "      model.layer2.3.conv3.weight       paramerters num: 65536\n",
      "       model.layer2.3.bn3.weight        paramerters num: 512\n",
      "        model.layer2.3.bn3.bias         paramerters num: 512\n",
      "      model.layer3.0.conv1.weight       paramerters num: 131072\n",
      "       model.layer3.0.bn1.weight        paramerters num: 256\n",
      "        model.layer3.0.bn1.bias         paramerters num: 256\n",
      "      model.layer3.0.conv2.weight       paramerters num: 589824\n",
      "       model.layer3.0.bn2.weight        paramerters num: 256\n",
      "        model.layer3.0.bn2.bias         paramerters num: 256\n",
      "      model.layer3.0.conv3.weight       paramerters num: 262144\n",
      "       model.layer3.0.bn3.weight        paramerters num: 1024\n",
      "        model.layer3.0.bn3.bias         paramerters num: 1024\n",
      "   model.layer3.0.downsample.0.weight   paramerters num: 524288\n",
      "   model.layer3.0.downsample.1.weight   paramerters num: 1024\n",
      "    model.layer3.0.downsample.1.bias    paramerters num: 1024\n",
      "      model.layer3.1.conv1.weight       paramerters num: 262144\n",
      "       model.layer3.1.bn1.weight        paramerters num: 256\n",
      "        model.layer3.1.bn1.bias         paramerters num: 256\n",
      "      model.layer3.1.conv2.weight       paramerters num: 589824\n",
      "       model.layer3.1.bn2.weight        paramerters num: 256\n",
      "        model.layer3.1.bn2.bias         paramerters num: 256\n",
      "      model.layer3.1.conv3.weight       paramerters num: 262144\n",
      "       model.layer3.1.bn3.weight        paramerters num: 1024\n",
      "        model.layer3.1.bn3.bias         paramerters num: 1024\n",
      "      model.layer3.2.conv1.weight       paramerters num: 262144\n",
      "       model.layer3.2.bn1.weight        paramerters num: 256\n",
      "        model.layer3.2.bn1.bias         paramerters num: 256\n",
      "      model.layer3.2.conv2.weight       paramerters num: 589824\n",
      "       model.layer3.2.bn2.weight        paramerters num: 256\n",
      "        model.layer3.2.bn2.bias         paramerters num: 256\n",
      "      model.layer3.2.conv3.weight       paramerters num: 262144\n",
      "       model.layer3.2.bn3.weight        paramerters num: 1024\n",
      "        model.layer3.2.bn3.bias         paramerters num: 1024\n",
      "      model.layer3.3.conv1.weight       paramerters num: 262144\n",
      "       model.layer3.3.bn1.weight        paramerters num: 256\n",
      "        model.layer3.3.bn1.bias         paramerters num: 256\n",
      "      model.layer3.3.conv2.weight       paramerters num: 589824\n",
      "       model.layer3.3.bn2.weight        paramerters num: 256\n",
      "        model.layer3.3.bn2.bias         paramerters num: 256\n",
      "      model.layer3.3.conv3.weight       paramerters num: 262144\n",
      "       model.layer3.3.bn3.weight        paramerters num: 1024\n",
      "        model.layer3.3.bn3.bias         paramerters num: 1024\n",
      "      model.layer3.4.conv1.weight       paramerters num: 262144\n",
      "       model.layer3.4.bn1.weight        paramerters num: 256\n",
      "        model.layer3.4.bn1.bias         paramerters num: 256\n",
      "      model.layer3.4.conv2.weight       paramerters num: 589824\n",
      "       model.layer3.4.bn2.weight        paramerters num: 256\n",
      "        model.layer3.4.bn2.bias         paramerters num: 256\n",
      "      model.layer3.4.conv3.weight       paramerters num: 262144\n",
      "       model.layer3.4.bn3.weight        paramerters num: 1024\n",
      "        model.layer3.4.bn3.bias         paramerters num: 1024\n",
      "      model.layer3.5.conv1.weight       paramerters num: 262144\n",
      "       model.layer3.5.bn1.weight        paramerters num: 256\n",
      "        model.layer3.5.bn1.bias         paramerters num: 256\n",
      "      model.layer3.5.conv2.weight       paramerters num: 589824\n",
      "       model.layer3.5.bn2.weight        paramerters num: 256\n",
      "        model.layer3.5.bn2.bias         paramerters num: 256\n",
      "      model.layer3.5.conv3.weight       paramerters num: 262144\n",
      "       model.layer3.5.bn3.weight        paramerters num: 1024\n",
      "        model.layer3.5.bn3.bias         paramerters num: 1024\n",
      "      model.layer4.0.conv1.weight       paramerters num: 524288\n",
      "       model.layer4.0.bn1.weight        paramerters num: 512\n",
      "        model.layer4.0.bn1.bias         paramerters num: 512\n",
      "      model.layer4.0.conv2.weight       paramerters num: 2359296\n",
      "       model.layer4.0.bn2.weight        paramerters num: 512\n",
      "        model.layer4.0.bn2.bias         paramerters num: 512\n",
      "      model.layer4.0.conv3.weight       paramerters num: 1048576\n",
      "       model.layer4.0.bn3.weight        paramerters num: 2048\n",
      "        model.layer4.0.bn3.bias         paramerters num: 2048\n",
      "   model.layer4.0.downsample.0.weight   paramerters num: 2097152\n",
      "   model.layer4.0.downsample.1.weight   paramerters num: 2048\n",
      "    model.layer4.0.downsample.1.bias    paramerters num: 2048\n",
      "      model.layer4.1.conv1.weight       paramerters num: 1048576\n",
      "       model.layer4.1.bn1.weight        paramerters num: 512\n",
      "        model.layer4.1.bn1.bias         paramerters num: 512\n",
      "      model.layer4.1.conv2.weight       paramerters num: 2359296\n",
      "       model.layer4.1.bn2.weight        paramerters num: 512\n",
      "        model.layer4.1.bn2.bias         paramerters num: 512\n",
      "      model.layer4.1.conv3.weight       paramerters num: 1048576\n",
      "       model.layer4.1.bn3.weight        paramerters num: 2048\n",
      "        model.layer4.1.bn3.bias         paramerters num: 2048\n",
      "      model.layer4.2.conv1.weight       paramerters num: 1048576\n",
      "       model.layer4.2.bn1.weight        paramerters num: 512\n",
      "        model.layer4.2.bn1.bias         paramerters num: 512\n",
      "      model.layer4.2.conv2.weight       paramerters num: 2359296\n",
      "       model.layer4.2.bn2.weight        paramerters num: 512\n",
      "        model.layer4.2.bn2.bias         paramerters num: 512\n",
      "      model.layer4.2.conv3.weight       paramerters num: 1048576\n",
      "       model.layer4.2.bn3.weight        paramerters num: 2048\n",
      "        model.layer4.2.bn3.bias         paramerters num: 2048\n",
      "            model.fc.weight             paramerters num: 20480\n",
      "             model.fc.bias              paramerters num: 10\n"
     ]
    }
   ],
   "execution_count": 8
  },
  {
   "cell_type": "code",
   "source": [
    "model = ResNet50(num_classes=10, frozen=True)"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2025-02-02T12:57:02.932366Z",
     "start_time": "2025-02-02T12:57:02.652181Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2048\n",
      "1000\n"
     ]
    }
   ],
   "execution_count": 9
  },
  {
   "cell_type": "code",
   "source": [
    "model"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2025-02-02T12:57:02.939121Z",
     "start_time": "2025-02-02T12:57:02.933353Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "ResNet50(\n",
       "  (model): ResNet(\n",
       "    (conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)\n",
       "    (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "    (relu): ReLU(inplace=True)\n",
       "    (maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)\n",
       "    (layer1): Sequential(\n",
       "      (0): Bottleneck(\n",
       "        (conv1): Conv2d(64, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "        (downsample): Sequential(\n",
       "          (0): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "          (1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        )\n",
       "      )\n",
       "      (1): Bottleneck(\n",
       "        (conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (2): Bottleneck(\n",
       "        (conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "    )\n",
       "    (layer2): Sequential(\n",
       "      (0): Bottleneck(\n",
       "        (conv1): Conv2d(256, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "        (downsample): Sequential(\n",
       "          (0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)\n",
       "          (1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        )\n",
       "      )\n",
       "      (1): Bottleneck(\n",
       "        (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (2): Bottleneck(\n",
       "        (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (3): Bottleneck(\n",
       "        (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "    )\n",
       "    (layer3): Sequential(\n",
       "      (0): Bottleneck(\n",
       "        (conv1): Conv2d(512, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "        (downsample): Sequential(\n",
       "          (0): Conv2d(512, 1024, kernel_size=(1, 1), stride=(2, 2), bias=False)\n",
       "          (1): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        )\n",
       "      )\n",
       "      (1): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (2): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (3): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (4): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (5): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "    )\n",
       "    (layer4): Sequential(\n",
       "      (0): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "        (downsample): Sequential(\n",
       "          (0): Conv2d(1024, 2048, kernel_size=(1, 1), stride=(2, 2), bias=False)\n",
       "          (1): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        )\n",
       "      )\n",
       "      (1): Bottleneck(\n",
       "        (conv1): Conv2d(2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (2): Bottleneck(\n",
       "        (conv1): Conv2d(2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "    )\n",
       "    (avgpool): AdaptiveAvgPool2d(output_size=(1, 1))\n",
       "    (fc): Linear(in_features=2048, out_features=10, bias=True)\n",
       "  )\n",
       ")"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 10
  },
  {
   "cell_type": "code",
   "source": [
    "total_params = sum(p.numel() for p in model.parameters() )\n",
    "print(f\"Total trainable parameters: {total_params}\")"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2025-02-02T12:57:02.948697Z",
     "start_time": "2025-02-02T12:57:02.940118Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Total trainable parameters: 23528522\n"
     ]
    }
   ],
   "execution_count": 11
  },
  {
   "cell_type": "code",
   "source": [],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2025-02-02T12:57:02.951451Z",
     "start_time": "2025-02-02T12:57:02.948697Z"
    }
   },
   "outputs": [],
   "execution_count": 11
  },
  {
   "cell_type": "code",
   "source": [
    "m = nn.AdaptiveAvgPool2d(output_size=(1, 1))\n",
    "input = torch.randn(1, 2048, 9, 9)\n",
    "output = m(input)\n",
    "output.shape"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2025-02-02T12:57:02.964225Z",
     "start_time": "2025-02-02T12:57:02.952451Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "torch.Size([1, 2048, 1, 1])"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 12
  },
  {
   "cell_type": "code",
   "source": [
    "512*3*3*512"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2025-02-02T12:57:02.970204Z",
     "start_time": "2025-02-02T12:57:02.965221Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "2359296"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 13
  },
  {
   "cell_type": "code",
   "source": [
    "512*1*1*2048"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2025-02-02T12:57:02.981036Z",
     "start_time": "2025-02-02T12:57:02.970204Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "1048576"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 14
  },
  {
   "cell_type": "code",
   "source": [
    "512/16"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2025-02-02T12:57:02.991192Z",
     "start_time": "2025-02-02T12:57:02.982263Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "32.0"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 15
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 训练\n",
    "\n",
    "pytorch的训练需要自行实现，包括\n",
    "1. 定义损失函数\n",
    "2. 定义优化器\n",
    "3. 定义训练步\n",
    "4. 训练"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-02T12:57:03.076998Z",
     "start_time": "2025-02-02T12:57:02.992187Z"
    }
   },
   "source": [
    "from sklearn.metrics import accuracy_score\n",
    "\n",
    "@torch.no_grad()\n",
    "def evaluating(model, dataloader, loss_fct):\n",
    "    loss_list = []\n",
    "    pred_list = []\n",
    "    label_list = []\n",
    "    for datas, labels in dataloader:\n",
    "        datas = datas.to(device)\n",
    "        labels = labels.to(device)\n",
    "        # 前向计算\n",
    "        logits = model(datas)\n",
    "        loss = loss_fct(logits, labels)         # 验证集损失\n",
    "        loss_list.append(loss.item())\n",
    "        \n",
    "        preds = logits.argmax(axis=-1)    # 验证集预测\n",
    "        pred_list.extend(preds.cpu().numpy().tolist())\n",
    "        label_list.extend(labels.cpu().numpy().tolist())\n",
    "        \n",
    "    acc = accuracy_score(label_list, pred_list)\n",
    "    return np.mean(loss_list), acc\n"
   ],
   "outputs": [],
   "execution_count": 16
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### TensorBoard 可视化\n",
    "\n",
    "\n",
    "训练过程中可以使用如下命令启动tensorboard服务。\n",
    "\n",
    "```shell\n",
    "tensorboard \\\n",
    "    --logdir=runs \\     # log 存放路径\n",
    "    --host 0.0.0.0 \\    # ip\n",
    "    --port 8848         # 端口\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-02T12:57:06.991056Z",
     "start_time": "2025-02-02T12:57:03.079208Z"
    }
   },
   "source": [
    "from torch.utils.tensorboard import SummaryWriter\n",
    "\n",
    "\n",
    "class TensorBoardCallback:\n",
    "    def __init__(self, log_dir, flush_secs=10):\n",
    "        \"\"\"\n",
    "        Args:\n",
    "            log_dir (str): dir to write log.\n",
    "            flush_secs (int, optional): write to dsk each flush_secs seconds. Defaults to 10.\n",
    "        \"\"\"\n",
    "        self.writer = SummaryWriter(log_dir=log_dir, flush_secs=flush_secs)\n",
    "\n",
    "    def draw_model(self, model, input_shape):\n",
    "        self.writer.add_graph(model, input_to_model=torch.randn(input_shape))\n",
    "        \n",
    "    def add_loss_scalars(self, step, loss, val_loss):\n",
    "        self.writer.add_scalars(\n",
    "            main_tag=\"training/loss\", \n",
    "            tag_scalar_dict={\"loss\": loss, \"val_loss\": val_loss},\n",
    "            global_step=step,\n",
    "            )\n",
    "        \n",
    "    def add_acc_scalars(self, step, acc, val_acc):\n",
    "        self.writer.add_scalars(\n",
    "            main_tag=\"training/accuracy\",\n",
    "            tag_scalar_dict={\"accuracy\": acc, \"val_accuracy\": val_acc},\n",
    "            global_step=step,\n",
    "        )\n",
    "        \n",
    "    def add_lr_scalars(self, step, learning_rate):\n",
    "        self.writer.add_scalars(\n",
    "            main_tag=\"training/learning_rate\",\n",
    "            tag_scalar_dict={\"learning_rate\": learning_rate},\n",
    "            global_step=step,\n",
    "            \n",
    "        )\n",
    "    \n",
    "    def __call__(self, step, **kwargs):\n",
    "        # add loss\n",
    "        loss = kwargs.pop(\"loss\", None)\n",
    "        val_loss = kwargs.pop(\"val_loss\", None)\n",
    "        if loss is not None and val_loss is not None:\n",
    "            self.add_loss_scalars(step, loss, val_loss)\n",
    "        # add acc\n",
    "        acc = kwargs.pop(\"acc\", None)\n",
    "        val_acc = kwargs.pop(\"val_acc\", None)\n",
    "        if acc is not None and val_acc is not None:\n",
    "            self.add_acc_scalars(step, acc, val_acc)\n",
    "        # add lr\n",
    "        learning_rate = kwargs.pop(\"lr\", None)\n",
    "        if learning_rate is not None:\n",
    "            self.add_lr_scalars(step, learning_rate)\n"
   ],
   "outputs": [],
   "execution_count": 17
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Save Best\n"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-02T12:57:06.996026Z",
     "start_time": "2025-02-02T12:57:06.992029Z"
    }
   },
   "source": [
    "class SaveCheckpointsCallback:\n",
    "    def __init__(self, save_dir, save_step=5000, save_best_only=True):\n",
    "        \"\"\"\n",
    "        Save checkpoints each save_epoch epoch. \n",
    "        We save checkpoint by epoch in this implementation.\n",
    "        Usually, training scripts with pytorch evaluating model and save checkpoint by step.\n",
    "\n",
    "        Args:\n",
    "            save_dir (str): dir to save checkpoint\n",
    "            save_epoch (int, optional): the frequency to save checkpoint. Defaults to 1.\n",
    "            save_best_only (bool, optional): If True, only save the best model or save each model at every epoch.\n",
    "        \"\"\"\n",
    "        self.save_dir = save_dir\n",
    "        self.save_step = save_step\n",
    "        self.save_best_only = save_best_only\n",
    "        self.best_metrics = -1\n",
    "        \n",
    "        # mkdir\n",
    "        if not os.path.exists(self.save_dir):\n",
    "            os.mkdir(self.save_dir)\n",
    "        \n",
    "    def __call__(self, step, state_dict, metric=None):\n",
    "        if step % self.save_step > 0:\n",
    "            return\n",
    "        \n",
    "        if self.save_best_only:\n",
    "            assert metric is not None\n",
    "            if metric >= self.best_metrics:\n",
    "                # save checkpoints\n",
    "                torch.save(state_dict, os.path.join(self.save_dir, \"best.ckpt\"))\n",
    "                # update best metrics\n",
    "                self.best_metrics = metric\n",
    "        else:\n",
    "            torch.save(state_dict, os.path.join(self.save_dir, f\"{step}.ckpt\"))\n",
    "\n"
   ],
   "outputs": [],
   "execution_count": 18
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Early Stop"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-02T12:57:07.007232Z",
     "start_time": "2025-02-02T12:57:06.996026Z"
    }
   },
   "source": [
    "class EarlyStopCallback:\n",
    "    def __init__(self, patience=5, min_delta=0.01):\n",
    "        \"\"\"\n",
    "\n",
    "        Args:\n",
    "            patience (int, optional): Number of epochs with no improvement after which training will be stopped.. Defaults to 5.\n",
    "            min_delta (float, optional): Minimum change in the monitored quantity to qualify as an improvement, i.e. an absolute \n",
    "                change of less than min_delta, will count as no improvement. Defaults to 0.01.\n",
    "        \"\"\"\n",
    "        self.patience = patience\n",
    "        self.min_delta = min_delta\n",
    "        self.best_metric = -1\n",
    "        self.counter = 0\n",
    "        \n",
    "    def __call__(self, metric):\n",
    "        if metric >= self.best_metric + self.min_delta:\n",
    "            # update best metric\n",
    "            self.best_metric = metric\n",
    "            # reset counter \n",
    "            self.counter = 0\n",
    "        else: \n",
    "            self.counter += 1\n",
    "            \n",
    "    @property\n",
    "    def early_stop(self):\n",
    "        return self.counter >= self.patience\n"
   ],
   "outputs": [],
   "execution_count": 19
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-02T13:08:28.983807Z",
     "start_time": "2025-02-02T12:57:07.008225Z"
    }
   },
   "source": [
    "# 训练\n",
    "def training(\n",
    "    model, \n",
    "    train_loader, \n",
    "    val_loader, \n",
    "    epoch, \n",
    "    loss_fct, \n",
    "    optimizer, \n",
    "    tensorboard_callback=None,\n",
    "    save_ckpt_callback=None,\n",
    "    early_stop_callback=None,\n",
    "    eval_step=500,\n",
    "    ):\n",
    "    record_dict = {\n",
    "        \"train\": [],\n",
    "        \"val\": []\n",
    "    }\n",
    "    \n",
    "    global_step = 0\n",
    "    model.train()\n",
    "    with tqdm(total=epoch * len(train_loader)) as pbar:\n",
    "        for epoch_id in range(epoch):\n",
    "            # training\n",
    "            for datas, labels in train_loader:\n",
    "                datas = datas.to(device)\n",
    "                labels = labels.to(device)\n",
    "                # 梯度清空\n",
    "                optimizer.zero_grad()\n",
    "                # 模型前向计算\n",
    "                logits = model(datas)\n",
    "                # 计算损失\n",
    "                loss = loss_fct(logits, labels)\n",
    "                # 梯度回传\n",
    "                loss.backward()\n",
    "                # 调整优化器，包括学习率的变动等\n",
    "                optimizer.step()\n",
    "                preds = logits.argmax(axis=-1)\n",
    "            \n",
    "                acc = accuracy_score(labels.cpu().numpy(), preds.cpu().numpy())    \n",
    "                loss = loss.cpu().item()\n",
    "                # record\n",
    "                \n",
    "                record_dict[\"train\"].append({\n",
    "                    \"loss\": loss, \"acc\": acc, \"step\": global_step\n",
    "                })\n",
    "                \n",
    "                # evaluating\n",
    "                if global_step % eval_step == 0:\n",
    "                    model.eval()\n",
    "                    val_loss, val_acc = evaluating(model, val_loader, loss_fct)\n",
    "                    record_dict[\"val\"].append({\n",
    "                        \"loss\": val_loss, \"acc\": val_acc, \"step\": global_step\n",
    "                    })\n",
    "                    model.train()\n",
    "                    \n",
    "                    # 1. 使用 tensorboard 可视化\n",
    "                    if tensorboard_callback is not None:\n",
    "                        tensorboard_callback(\n",
    "                            global_step, \n",
    "                            loss=loss, val_loss=val_loss,\n",
    "                            acc=acc, val_acc=val_acc,\n",
    "                            lr=optimizer.param_groups[0][\"lr\"],\n",
    "                            )\n",
    "                \n",
    "                    # 2. 保存模型权重 save model checkpoint\n",
    "                    if save_ckpt_callback is not None:\n",
    "                        save_ckpt_callback(global_step, model.state_dict(), metric=val_acc)\n",
    "\n",
    "                    # 3. 早停 Early Stop\n",
    "                    if early_stop_callback is not None:\n",
    "                        early_stop_callback(val_acc)\n",
    "                        if early_stop_callback.early_stop:\n",
    "                            print(f\"Early stop at epoch {epoch_id} / global_step {global_step}\")\n",
    "                            return record_dict\n",
    "                    \n",
    "                # udate step\n",
    "                global_step += 1\n",
    "                pbar.update(1)\n",
    "                pbar.set_postfix({\"epoch\": epoch_id})\n",
    "        \n",
    "    return record_dict\n",
    "        \n",
    "\n",
    "epoch = 20\n",
    "\n",
    "model = ResNet50(num_classes=10)\n",
    "\n",
    "# 1. 定义损失函数 采用交叉熵损失\n",
    "loss_fct = nn.CrossEntropyLoss()\n",
    "# 2. 定义优化器 采用 sgd\n",
    "# Optimizers specified in the torch.optim package\n",
    "optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.0)\n",
    "\n",
    "# 1. tensorboard 可视化\n",
    "if not os.path.exists(\"runs\"):\n",
    "    os.mkdir(\"runs\")\n",
    "# tensorboard_callback = TensorBoardCallback(\"runs/monkeys-resnet50\")\n",
    "# tensorboard_callback.draw_model(model, [1, 3, img_h, img_w])\n",
    "# 2. save best\n",
    "if not os.path.exists(\"checkpoints\"):\n",
    "    os.makedirs(\"checkpoints\")\n",
    "save_ckpt_callback = SaveCheckpointsCallback(\"checkpoints/monkeys-resnet50\", save_step=len(train_loader), save_best_only=True)\n",
    "# 3. early stop\n",
    "early_stop_callback = EarlyStopCallback(patience=5)\n",
    "\n",
    "model = model.to(device)\n",
    "record = training(\n",
    "    model, \n",
    "    train_loader, \n",
    "    val_loader, \n",
    "    epoch, \n",
    "    loss_fct, \n",
    "    optimizer, \n",
    "    tensorboard_callback=None,\n",
    "    save_ckpt_callback=save_ckpt_callback,\n",
    "    early_stop_callback=early_stop_callback,\n",
    "    eval_step=len(train_loader)\n",
    "    )"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2048\n",
      "1000\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "  0%|          | 0/1380 [00:00<?, ?it/s]"
      ],
      "application/vnd.jupyter.widget-view+json": {
       "version_major": 2,
       "version_minor": 0,
       "model_id": "4c70849264e2438896f0e68a2f874894"
      }
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Early stop at epoch 9 / global_step 621\n"
     ]
    }
   ],
   "execution_count": 20
  },
  {
   "cell_type": "code",
   "source": [
    "# 画图\n",
    "\n",
    "import torch\n",
    "from torchviz import make_dot\n",
    "\n",
    "# Assuming your model is already defined and named 'model'\n",
    "# Construct a dummy input\n",
    "dummy_input = torch.randn(1, 3, 224, 224)  # Replace with your input shape\n",
    "\n",
    "# Forward pass to generate the computation graph\n",
    "output = model(dummy_input)\n",
    "\n",
    "# Visualize the model architecture\n",
    "dot = make_dot(output, params=dict(model.named_parameters()))\n",
    "dot.render(\"model_architecture\", format=\"png\")  # Save the visualization as an image\n"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2025-02-02T13:08:29.250238Z",
     "start_time": "2025-02-02T13:08:28.984804Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'model_architecture.png'"
      ]
     },
     "execution_count": 21,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 21
  },
  {
   "cell_type": "code",
   "source": [],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2025-02-02T13:08:29.253629Z",
     "start_time": "2025-02-02T13:08:29.251234Z"
    }
   },
   "outputs": [],
   "execution_count": 21
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-02T13:08:29.388801Z",
     "start_time": "2025-02-02T13:08:29.253629Z"
    }
   },
   "source": [
    "#画线要注意的是损失是不一定在零到1之间的\n",
    "def plot_learning_curves(record_dict, sample_step=500):\n",
    "    # build DataFrame\n",
    "    train_df = pd.DataFrame(record_dict[\"train\"]).set_index(\"step\").iloc[::sample_step]\n",
    "    val_df = pd.DataFrame(record_dict[\"val\"]).set_index(\"step\")\n",
    "\n",
    "    # plot\n",
    "    fig_num = len(train_df.columns)\n",
    "    fig, axs = plt.subplots(1, fig_num, figsize=(5 * fig_num, 5))\n",
    "    for idx, item in enumerate(train_df.columns):    \n",
    "        axs[idx].plot(train_df.index, train_df[item], label=f\"train_{item}\")\n",
    "        axs[idx].plot(val_df.index, val_df[item], label=f\"val_{item}\")\n",
    "        axs[idx].grid()\n",
    "        axs[idx].legend()\n",
    "        # axs[idx].set_xticks(range(0, train_df.index[-1], 5000))\n",
    "        # axs[idx].set_xticklabels(map(lambda x: f\"{int(x/1000)}k\", range(0, train_df.index[-1], 5000)))\n",
    "        axs[idx].set_xlabel(\"step\")\n",
    "    \n",
    "    plt.show()\n",
    "\n",
    "plot_learning_curves(record, sample_step=10)  #横坐标是 steps"
   ],
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<Figure size 1000x500 with 2 Axes>"
      ],
      "image/png": "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"
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "execution_count": 22
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 评估"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-02T13:12:10.634470Z",
     "start_time": "2025-02-02T13:11:57.579459Z"
    }
   },
   "source": [
    "# dataload for evaluating\n",
    "\n",
    "# load checkpoints\n",
    "model.load_state_dict(torch.load(\"checkpoints/monkeys-resnet50/best.ckpt\",weights_only=True, map_location=\"cpu\"))\n",
    "\n",
    "model.eval()\n",
    "loss, acc = evaluating(model, val_loader, loss_fct)\n",
    "print(f\"loss:     {loss:.4f}\\naccuracy: {acc:.4f}\")"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "loss:     0.2286\n",
      "accuracy: 0.9853\n"
     ]
    }
   ],
   "execution_count": 24
  },
  {
   "metadata": {},
   "cell_type": "code",
   "outputs": [],
   "execution_count": null,
   "source": ""
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "pytorch",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.8"
  },
  "orig_nbformat": 4
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
